"""Generate and run C code.
"""

# Copyright The Mbed TLS Contributors
# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
#

import os
import platform
import subprocess
import sys
import tempfile

class CompileError(Exception):
    """Exception to represent an error during the compilation."""

    def __init__(self, message):
        """Save the error massage"""

        super().__init__()
        self.message = message

def remove_file_if_exists(filename):
    """Remove the specified file, ignoring errors."""
    if not filename:
        return
    try:
        os.remove(filename)
    except OSError:
        pass

def create_c_file(file_label):
    """Create a temporary C file.

    * ``file_label``: a string that will be included in the file name.

    Return ```(c_file, c_name, exe_name)``` where ``c_file`` is a Python
    stream open for writing to the file, ``c_name`` is the name of the file
    and ``exe_name`` is the name of the executable that will be produced
    by compiling the file.
    """
    c_fd, c_name = tempfile.mkstemp(prefix='tmp-{}-'.format(file_label),
                                    suffix='.c')
    exe_suffix = '.exe' if platform.system() == 'Windows' else ''
    exe_name = c_name[:-2] + exe_suffix
    remove_file_if_exists(exe_name)
    c_file = os.fdopen(c_fd, 'w', encoding='ascii')
    return c_file, c_name, exe_name

def generate_c_printf_expressions(c_file, cast_to, printf_format, expressions):
    """Generate C instructions to print the value of ``expressions``.

    Write the code with ``c_file``'s ``write`` method.

    Each expression is cast to the type ``cast_to`` and printed with the
    printf format ``printf_format``.
    """
    for expr in expressions:
        c_file.write('    printf("{}\\n", ({}) {});\n'
                     .format(printf_format, cast_to, expr))

def generate_c_file(c_file,
                    caller, header,
                    main_generator):
    """Generate a temporary C source file.

    * ``c_file`` is an open stream on the C source file.
    * ``caller``: an informational string written in a comment at the top
      of the file.
    * ``header``: extra code to insert before any function in the generated
      C file.
    * ``main_generator``: a function called with ``c_file`` as its sole argument
      to generate the body of the ``main()`` function.
    """
    c_file.write('/* Generated by {} */'
                 .format(caller))
    c_file.write('''
#include <stdio.h>
''')
    c_file.write(header)
    c_file.write('''
int main(void)
{
''')
    main_generator(c_file)
    c_file.write('''    return 0;
}
''')

def compile_c_file(c_filename, exe_filename, include_dirs):
    """Compile a C source file with the host compiler.

    * ``c_filename``: the name of the source file to compile.
    * ``exe_filename``: the name for the executable to be created.
    * ``include_dirs``: a list of paths to include directories to be passed
      with the -I switch.
    """
    # Respect $HOSTCC if it is set
    cc = os.getenv('HOSTCC', None)
    if cc is None:
        cc = os.getenv('CC', 'cc')
    cmd = [cc]

    proc = subprocess.Popen(cmd,
                            stdout=subprocess.DEVNULL,
                            stderr=subprocess.PIPE,
                            universal_newlines=True)
    cc_is_msvc = 'Microsoft (R) C/C++' in proc.communicate()[1]

    cmd += ['-I' + dir for dir in include_dirs]
    if cc_is_msvc:
        # MSVC has deprecated using -o to specify the output file,
        # and produces an object file in the working directory by default.
        obj_filename = exe_filename[:-4] + '.obj'
        cmd += ['-Fe' + exe_filename, '-Fo' + obj_filename]
    else:
        cmd += ['-o' + exe_filename]

    try:
        subprocess.check_output(cmd + [c_filename],
                                stderr=subprocess.PIPE,
                                universal_newlines=True)

    except subprocess.CalledProcessError as e:
        raise CompileError(e.stderr) from e

def get_c_expression_values(
        cast_to, printf_format,
        expressions,
        caller=__name__, file_label='',
        header='', include_path=None,
        keep_c=False,
): # pylint: disable=too-many-arguments, too-many-locals
    """Generate and run a program to print out numerical values for expressions.

    * ``cast_to``: a C type.
    * ``printf_format``: a printf format suitable for the type ``cast_to``.
    * ``header``: extra code to insert before any function in the generated
      C file.
    * ``expressions``: a list of C language expressions that have the type
      ``cast_to``.
    * ``include_path``: a list of directories containing header files.
    * ``keep_c``: if true, keep the temporary C file (presumably for debugging
      purposes).

    Use the C compiler specified by the ``CC`` environment variable, defaulting
    to ``cc``. If ``CC`` looks like MSVC, use its command line syntax,
    otherwise assume the compiler supports Unix traditional ``-I`` and ``-o``.

    Return the list of values of the ``expressions``.
    """
    if include_path is None:
        include_path = []
    c_name = None
    exe_name = None
    obj_name = None
    try:
        c_file, c_name, exe_name = create_c_file(file_label)
        generate_c_file(
            c_file, caller, header,
            lambda c_file: generate_c_printf_expressions(c_file,
                                                         cast_to, printf_format,
                                                         expressions)
        )
        c_file.close()

        compile_c_file(c_name, exe_name, include_path)
        if keep_c:
            sys.stderr.write('List of {} tests kept at {}\n'
                             .format(caller, c_name))
        else:
            os.remove(c_name)
        output = subprocess.check_output([exe_name])
        return output.decode('ascii').strip().split('\n')
    finally:
        remove_file_if_exists(exe_name)
        remove_file_if_exists(obj_name)
